/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package io.iec.edp.caf.commons.configclient.locator;


import io.iec.edp.caf.commons.configclient.api.CAFPropertySourceLocator;
import io.iec.edp.caf.commons.configclient.entity.ConfigClientProperties;

import io.iec.edp.caf.commons.configclient.entity.ConfigClientStateHolder;
import io.iec.edp.caf.commons.configclient.entity.Environment;

import io.iec.edp.caf.commons.configclient.entity.PropertySource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Order(0)
public class ConfigServicePropertySourceLocator implements CAFPropertySourceLocator {

    private RestTemplate restTemplate;

    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }


    private ConfigClientProperties defaultProperties;

    public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) {
        this.defaultProperties = defaultProperties;
    }

    private RestTemplate getSecureRestTemplate(ConfigClientProperties client) {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        if (client.getRequestReadTimeout() < 0) {
            throw new IllegalStateException("Invalid Value for Read Timeout set.");
        }
        if (client.getRequestConnectTimeout() < 0) {
            throw new IllegalStateException("Invalid Value for Connect Timeout set.");
        }
        requestFactory.setReadTimeout(client.getRequestReadTimeout());
        requestFactory.setConnectTimeout(client.getRequestConnectTimeout());
        RestTemplate template = new RestTemplate(requestFactory);
        Map<String, String> headers = new HashMap<>(client.getHeaders());
        if (headers.containsKey(ConfigClientProperties.AUTHORIZATION)) {
            headers.remove(ConfigClientProperties.AUTHORIZATION); // To avoid redundant addition of header
        }

        if (!headers.isEmpty()) {
            template.setInterceptors(Arrays.asList(new ClientHttpRequestInterceptor[] { new GenericRequestHeaderInterceptor(headers) }));
        }

        return template;
    }

    private Environment getRemoteEnvironment(RestTemplate restTemplate, ConfigClientProperties properties, String label, String state) {
        String path = "/{name}/{profile}";
        String name = properties.getName();
        String profile = properties.getProfile();
        String token = properties.getToken();
        int noOfUrls = properties.getUri().length;
        if (noOfUrls > 1) {
            log.info("Multiple Config Server Urls found listed.");
        }

        Object[] args = new String[] { name, profile };
        if (StringUtils.hasText(label)) {
            if (label.contains("/")) {
                label = label.replace("/", "(_)");
            }
            args = new String[] { name, profile, label };
            path = path + "/{label}";
        }
        ResponseEntity<Environment> response = null;

        for (int i = 0; i < noOfUrls; i++) {
            ConfigClientProperties.Credentials credentials = properties.getCredentials(i);
            String uri = credentials.getUri();
            String username = credentials.getUsername();
            String password = credentials.getPassword();

            log.info("Fetching config from server at : " + uri);

            try {
                HttpHeaders headers = new HttpHeaders();
                addAuthorizationToken(properties, headers, username, password);
                if (StringUtils.hasText(token)) {
                    headers.add(ConfigClientProperties.TOKEN_HEADER, token);
                }
                if (StringUtils.hasText(state) && properties.isSendState()) {
                    headers.add(ConfigClientProperties.STATE_HEADER, state);
                }
                headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));

                final HttpEntity<Void> entity = new HttpEntity<>((Void) null, headers);
                response = restTemplate.exchange(uri + path, HttpMethod.GET, entity,
                        Environment.class, args);
            }
            catch (HttpClientErrorException e) {
                if (e.getStatusCode() != HttpStatus.NOT_FOUND) {
                    throw e;
                }
            }
            catch (ResourceAccessException e) {
                log.info("Connect Timeout Exception on Url - " + uri + ". Will be trying the next url if available");
                if (i == noOfUrls - 1) {
                    throw e;
                }
                else {
                    continue;
                }
            }

            if (response == null || response.getStatusCode() != HttpStatus.OK) {
                return null;
            }

            Environment result = response.getBody();
            return result;
        }

        return null;
    }

    private void addAuthorizationToken(ConfigClientProperties configClientProperties,
                                       HttpHeaders httpHeaders, String username, String password) {
        String authorization = configClientProperties.getHeaders().get(ConfigClientProperties.AUTHORIZATION);

        if (password != null && authorization != null) {
            throw new IllegalStateException("You must set either 'password' or 'authorization'");
        }

        if (password != null) {
            byte[] token = Base64Utils.encode((username + ":" + password).getBytes());
            httpHeaders.add("Authorization", "Basic " + new String(token));
        }
        else if (authorization != null) {
            httpHeaders.add("Authorization", authorization);
        }

    }

    @Override
    public org.springframework.core.env.PropertySource<?> locate(org.springframework.core.env.Environment environment) {
        ConfigClientProperties properties = this.defaultProperties.override(environment);
        CompositePropertySource composite = new CompositePropertySource("configService");
        RestTemplate restTemplate = this.restTemplate == null ? getSecureRestTemplate(properties) : this.restTemplate;
        Exception error = null;
        String errorBody = null;

        if(properties.getUri().length>0){
            try {
                String[] labels = new String[] { "" };
                if (StringUtils.hasText(properties.getLabel())) {
                    labels = StringUtils.commaDelimitedListToStringArray(properties.getLabel());
                }
                String state = ConfigClientStateHolder.getState();
                // Try all the labels until one works
                for (String label : labels) {
                    Environment result = getRemoteEnvironment(restTemplate, properties,label.trim(), state);
                    if (result != null) {
                        log.info("Evironment: ",result);

                        if (result.getPropertySources() != null) { // result.getPropertySources()
                            // can be null if using
                            // xml
                            for (PropertySource source : result.getPropertySources()) {
                                @SuppressWarnings("unchecked")
                                Map<String, Object> map = (Map<String, Object>) source.getSource();
                                composite.addPropertySource(new MapPropertySource(source.getName(), map));
                            }
                        }

                        if (StringUtils.hasText(result.getState()) || StringUtils.hasText(result.getVersion())) {
                            HashMap<String, Object> map = new HashMap<>();
                            putValue(map, "config.client.state", result.getState());
                            putValue(map, "config.client.version", result.getVersion());
                            composite.addFirstPropertySource(new MapPropertySource("configClient", map));
                        }
                        return composite;
                    }
                }
            }
            catch (HttpServerErrorException e) {
                error = e;
                if (MediaType.APPLICATION_JSON.includes(e.getResponseHeaders().getContentType())) {
                    errorBody = e.getResponseBodyAsString();
                }
            }
            catch (Exception e) {
                error = e;
            }
            if (properties.isFailFast()) {
                throw new IllegalStateException("Could not locate PropertySource and the fail fast property is set, failing"+ (errorBody == null ? "" : ": " + errorBody),error);
            }
            log.warn("Could not locate PropertySource: " + (errorBody == null ? error == null ? "label not found" : error.getMessage() : errorBody));
            return null;

        }

        return null;
    }


    private void putValue(HashMap<String, Object> map, String key, String value) {
        if (StringUtils.hasText(value)) {
            map.put(key, value);
        }
    }

    public static class GenericRequestHeaderInterceptor implements ClientHttpRequestInterceptor
    {
        private final Map<String, String> headers;

        public GenericRequestHeaderInterceptor(Map<String, String> headers) { this.headers = headers; }

        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            for (Map.Entry<String, String> header : this.headers.entrySet()) {
                request.getHeaders().add((String)header.getKey(), (String)header.getValue());
            }
            return execution.execute(request, body);
        }

        protected Map<String, String> getHeaders() { return this.headers; }
    }
}
